有經驗的前端工程師或多或少應該都有聽過 MVC
、MVP
、MVVM
架構的開發方式,這些開發方式可以讓我們達到觀注點分離(Separation of concerns,SoC)的設計原則讓開發團隊可以遵詢同一種模式進行開發工作。
今天我們就來看看目前在 Flutter 設計上經常聽到 Bloc
是什麼吧。
Bloc (Business Logic Component) 的設計理念會希望透過該設計原則將 View
的代碼與業務邏輯拆開, 易於程式碼的維護與開發、測試。
a predictable state management library for Dart.
Simple & Lightweight
Highly Testable
For Dart, Flutter, and AngularDart
從官網的架構圖上來看,從角色可以區分為三種類別
該圖片引用來自官網架構文件說明
負責處理資料來源的管理,通常會從 DB
或是 API
取得資料。
接收 UI
傳遞過來的事件(events)觸發業務邏輯的處理,可能會需要從 Data
取得相關資料,視邏輯有機會觸發狀態(states)的轉換。
負責處理畫面的呈現,畫面照業務邏輯的 states
而有不同狀態的顯示方式。
在開發前需要定義應用上可能的狀態以及會需要處理的事件為何!!!
先前的聊天室範例我們是使用 StatefulWidget
搭配 ViewModel
的寫法,接下來我們試著用 bloc
改寫看看。
與 bloc 相關的套件如下
dependencies:
bloc: ^7.2.0
flutter_bloc: ^7.3.0
equatable: ^2.0.3
負責處理聊天室WebSocket建立工作,新增一個類別繼承Bloc
並定義對應的Event
與State
。
class ChatBloc extends Bloc<ChatEvent, ChatState> {
final Connection _connection;
ChatBloc(this._connection) : super(const ChatState()) {}
}
Connection
是先前範例中我們包裝用來建立 WebSocket 的類別,在 bloc 初始化時從外部注入。
在ChatState
類別中我們定義兩個屬性
status
- 記錄目前連線狀態data
- 記錄聊天室訊息記錄enum SocketStatus { initial, open, closed }
class ChatState extends Equatable {
final SocketStatus status;
final List<Message> data;
const ChatState({
this.status = SocketStatus.initial,
this.data = const <Message>[],
});
ChatState copyWith({
SocketStatus? status,
List<Message>? data,
}) {
return ChatState(
status: status ?? this.status,
data: data ?? this.data,
);
}
@override
String toString() {
return '''ChatState { status: $status, data_length: ${data.length} }''';
}
@override
List<Object> get props => [data, status];
}
根據需求整理出聊天室會需要處理的事件內容
abstract class ChatEvent extends Equatable {
const ChatEvent();
@override
List<Object> get props => [];
}
class ChatReceiveMessage extends ChatEvent {
final Message msg;
const ChatReceiveMessage(this.msg);
}
class ChatSocketStatusChange extends ChatEvent {
final bool status;
const ChatSocketStatusChange(this.status);
}
class ChatDBInit extends ChatEvent {}
完成業務邏輯的基本定義後,接下來試著跟畫面結合在一下
使用 flutter_bloc
提供的 BlocProvider
提供聊天室 Bloc 的實例
class ChatPage extends StatelessWidget {
ChatPage({Key? key}) : super(key: key);
final Uri uri = Uri.parse('ws://test.dev.rde:8000/?token=sm2');
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => ChatBloc(Connection(uri: uri)),
child: const ChatView(),
);
}
}
在 ChatBloc
建構式需定義事件設定,這邊我們可以透過 bloc
的 add
方法觸發事件 ChatDBInit
。
早期 bloc 的寫法是在這邊定義
mapEventToState
,不過語法比較難理解,後續已建議改成下列的寫法。bloc_issues
我們在接收到 ChatDBInit
事件後透過 on
綁定 _initDB
處理,我們先從 db 取回訊息資料,並透過 ChatState copyWith
產生一個新的 state
,並以 bloc
的 emit
觸發狀態異動。
接著在綁定Connection
相關的事件:ChatSocketStatusChange
、ChatReceiveMessage
ChatBloc(this._connection) : super(const ChatState()) {
on<ChatReceiveMessage>(_onMessage);
on<ChatSocketStatusChange>(_onStateChange);
on<ChatDBInit>(_initDB);
add(ChatDBInit());
}
void _initDB(event, emit) async {
emit(state.copyWith(data: await db.query()));
_connectionSubscription = _connection.connected.listen((bool status) {
add(ChatSocketStatusChange(status));
});
_connectionSubscription = _connection.stream.listen((data) {
if (data["eventName"] == "chat:msg") {
add(ChatReceiveMessage(Message.fromJson(data)));
}
});
}
簡單來說:bloc
其實是有限狀態機
的一種設計方式,根據業務邏輯的需要歸納出states
,透過events
觸發業務邏輯的處理,引發 state
的轉換。
在 View
的處理上,可使用 flutter_bloc
提供的 BlocBuilder
監控狀態的變換而重新渲染畫面,並使用 state
裡與畫面有關的資料。
例如:我們在 ChatState 中定義 status 屬性處理 WebSocket 的連線狀態
Widget build(BuildContext context) {
return Expanded(
flex: 1,
child: BlocBuilder<ChatBloc, ChatState>(
builder: (context, state) {
final btnTitle = state.status == SocketStatus.open ? "已連線" : "請重連";
var controller = context.read<ChatBloc>().controller;
return Column(
children: [
SizedBox(
width: double.infinity,
child: TextButton(
child: Text(btnTitle),
onPressed: () {
print(state.status);
if (state.status == SocketStatus.closed) {
context.read<ChatBloc>().reconnect();
}
},
),
),
使用bloc
改寫後程式碼語意更易懂,也不用一直呼叫 setState
,完整程式碼在這
我自己在研究bloc
時初期遇到的狀況是不太曉得要怎麼將業務邏輯定義清楚以及states
、events
的內容要怎麼寫。後來查看官網上的一些範例後才慢慢掌握。
心得如下:
業務邏輯的單位大小由你自己決定:在聊天室的範例中,初期我一直在糾結連線狀態與聊天室訊息記錄是要放在一起還是拆分成兩個bloc
,其實沒有對與錯,就看自己怎麼寫符合當下狀況,有需要在拆分也行。
bloc
、cubit
兩種寫法哪一種適合我:如果你只要處理狀態資料的轉換而不用事件的狀態那簡單應用 cubit
就好。例如:其實我可以用 cubit
處理接收到聊天室的訊息事件即可。
states
的寫法:定義狀態抽象類別然後在依需求實作不同狀態的子類別還是 使用單一狀態類別透過 copyWith
產生新的狀態類別實例。
我要使用什麼方式觸發states
的轉換:在聊天室的範例我從 WebSocket 收到訊息時,我可以發出"接到訊息事件"對應後續的業務邏輯,也可以直接發出新的狀態,那我到底需不需要額外使用事件
follow 原則: 事件
> 業務邏輯
> 狀態
使用bloc
將觀察點分離,在組件中我們只要處理與 UI 有關的邏輯,將複雜多變的業務邏輯放到 bloc
,在開發與維護或是測試工作上都是很不錯的,建議花點時間研究。